// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ControlBoard.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Hardware.EV4
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Timers;

    using Microsoft.Robotics.Hardware.DAC;

    /// <summary>
    /// The Controlboard class
    /// </summary>
    public abstract class ControlBoard : IDisposable, IObservable<HeartbeatState>
    {
        /// <summary>
        /// The dictionary that holds commands awaiting responses, keyed
        /// by the sequence number the command was sent on.
        /// </summary>
        private readonly IDictionary<int, ICommand> awaitingResponses;

        /// <summary>
        /// The protocol used for this board
        /// </summary>
        private readonly IProtocol protocol;

        /// <summary>
        /// The transport
        /// </summary>
        private readonly ITransport transport;

        /// <summary>
        /// Synchronization object 
        /// </summary>
        private object syncRoot;

        /// <summary>
        /// Whether this instance is shutting down
        /// </summary>
        private bool shuttingDown;

        /// <summary>
        /// The board info
        /// </summary>
        private UnitInfo unitInfo;

        /// <summary>
        /// Most recent sensor info
        /// </summary>
        private byte[] sensorInfo;

        /// <summary>
        /// Timer to poll for heartbeats
        /// </summary>
        private Timer pollingTimer;

        /// <summary>
        /// The heartbeat polling interval
        /// </summary>
        private int keepAliveIntervalMS;

        /// <summary>
        /// The list of observers
        /// </summary>
        private List<IObserver<HeartbeatState>> observers;

        /// <summary>
        ///  Initializes a new instance of the <see cref="ControlBoard"/> class.
        /// </summary>
        /// <param name="transport">The transport to be used</param>
        /// <param name="protocol">The protocol to be used</param>
        /// <param name="keepAliveIntervalMS">The keep alive interval in milliseconds</param>
        protected ControlBoard(ITransport transport, IProtocol protocol, int keepAliveIntervalMS)
        {
            this.transport = transport;
            this.protocol = protocol;
            this.keepAliveIntervalMS = keepAliveIntervalMS;
            this.awaitingResponses = new Dictionary<int, ICommand>();
            this.syncRoot = new object();
            this.observers = new List<IObserver<HeartbeatState>>();
            this.IssueConnect();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Handles subscriptions to the HeartbeatState
        /// </summary>
        /// <param name="observer">Observer who wishes to receive updates</param>
        /// <returns>An IDisposable for unsubscribing</returns>
        public IDisposable Subscribe(IObserver<HeartbeatState> observer)
        {
            lock (this.syncRoot)
            {
                if (!this.observers.Contains(observer))
                {
                    this.observers.Add(observer);
                }
            }

            return new Unsubscriber(this.observers, observer, this.syncRoot);
        }

        /// <summary>
        /// Turn on host heartbeat
        /// </summary>
        public void KeepAlive()
        {
            this.pollingTimer = new Timer(this.keepAliveIntervalMS);
            this.pollingTimer.Elapsed += new ElapsedEventHandler(this.OnHostHeartbeatEvent);
            this.pollingTimer.Start();
        }

        /// <summary>
        /// Dispatches the command.
        /// </summary>
        /// <param name="cmd">The command.</param>
        public void DispatchCommand(ICommand cmd)
        {
            lock (this.syncRoot)
            {
                if (cmd.IsResponseRequired)
                {
                    this.awaitingResponses[this.protocol.GetNextSequenceNumber()] = cmd;
                }

                AsyncEventArgs writeAsyncArgs = this.transport.PrepareAsyncOperation(new byte[1024]);
                writeAsyncArgs.Completed += this.OnWriteCompletion;

                this.protocol.RunCommand(cmd, writeAsyncArgs);
            }
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                this.pollingTimer.Dispose();

                GC.SuppressFinalize(this);
            }
        }

        /// <summary>
        /// Processes the heartbeat data.
        /// </summary>
        /// <param name="buffer">The buffer.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="count">The count.</param>
        /// <returns>A HeartbeatState</returns>
        protected abstract HeartbeatState ProcessHeartbeatData(byte[] buffer, int offset, int count);

        /// <summary>
        /// Dispatches a request to get the control board specific unit info (FirmwareVersion etc)
        /// </summary>
        protected void SendGetUnitInfo()
        {
            GetUnitInfo getUnitInfo = new GetUnitInfo();
            this.DispatchCommand(getUnitInfo);
        }

        /// <summary>
        /// Dispatches a the GetSensorInfo command
        /// </summary>
        protected void SendGetSensorInfo()
        {
            GetSensorInfo getSensorInfo = new GetSensorInfo();
            this.DispatchCommand(getSensorInfo);
        }

        /// <summary>
        /// Issues the connect.
        /// </summary>
        private void IssueConnect()
        {
            if (this.shuttingDown)
            {
                return;
            }

            this.Connect();
        }

        /// <summary>
        /// Connects this instance.
        /// </summary>
        private void Connect()
        {
            try
            {
                this.transport.Connect();

                AsyncEventArgs readAsyncArgs = this.transport.PrepareAsyncOperation(new byte[1024]);
                readAsyncArgs.Completed += this.OnReadCompletion;
                this.transport.AsyncRead(readAsyncArgs);
            }
            catch (Exception)
            {
                this.IssueConnect();
            }
        }

        /// <summary>
        /// Disconnects this instance.
        /// </summary>
        /// <param name="terminating">If set to <c>true</c> [terminating].</param>
        private void Disconnect(bool terminating)
        {
            if (terminating)
            {
                this.shuttingDown = true;
            }

            this.transport.Disconnect();
        }

        /// <summary>
        /// Called upon read completion.
        /// </summary>
        /// <remarks>
        /// Attempts to extract message data and either reissues a read for the next heartbeat
        /// or reconnects upon failure.
        /// </remarks>
        /// <param name="sender">The sender.</param>
        /// <param name="eventArgs">The <see cref="MarsTpl.Api.Transport.AsyncEventArgs"/> instance containing the event data.</param>
        private void OnReadCompletion(object sender, AsyncEventArgs eventArgs)
        {
            if (eventArgs.LastError == null)
            {
                if (this.protocol.ExtractMessageData(eventArgs, this.ExtractCommandMessage))
                {
                    // read next heartbeat
                    AsyncEventArgs readAsyncArgs = this.transport.PrepareAsyncOperation(new byte[1024]);
                    readAsyncArgs.Completed += this.OnReadCompletion;
                    this.transport.AsyncRead(readAsyncArgs);
                }
                else
                {
                    this.IssueConnect();
                }
            }
        }

        /// <summary>
        /// Extracts and processes a CommandMessage from a DriveBoard data buffer
        /// </summary>
        /// <param name="buffer">The buffer</param>
        /// <param name="commandId">The commandid</param>
        /// <param name="seqno">The seqno</param>
        /// <param name="offset">The offset</param>
        /// <param name="length">The length</param>
        private void ExtractCommandMessage(byte[] buffer, byte commandId, int seqno, int offset, int length)
        {
            switch (commandId)
            {
                case (byte)CommandId.DeviceHeartbeat:
                    HeartbeatState state = this.ProcessHeartbeatData(buffer, offset, length);
                    lock (this.syncRoot)
                    {
                        foreach (IObserver<HeartbeatState> observer in this.observers)
                        {
                            observer.OnNext(state);
                        }
                    }

                    break;
                case (byte)CommandId.ErrorReport:
                    int responseCode = Marshal.ReadInt32(Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset));
                    lock (this.syncRoot)
                    {
                        foreach (IObserver<HeartbeatState> observer in this.observers)
                        {
                            observer.OnError(new FirmwareException(responseCode));
                        }
                    }

                    break;
                default:
                    ICommand command;
                    lock (this.syncRoot)
                    {
                        if (this.awaitingResponses.TryGetValue(seqno, out command))
                        {
                            this.awaitingResponses.Remove(seqno);

                            if (command != null && command.CommandType == commandId)
                            {
                                this.ParseParameters(command, buffer, offset, length);
                            }
                        }
                    }

                    break;
            }
        }

        /// <summary>
        /// Parses the parameters.
        /// </summary>
        /// <param name="command">The parser.</param>
        /// <param name="buffer">The buffer.</param>
        /// <param name="offset">The offset.</param>
        /// <param name="count">The count.</param>
        private void ParseParameters(ICommand command, byte[] buffer, int offset, int count)
        {
            MemoryStream ms = null;
            try
            {
                ms = new MemoryStream(buffer, offset, count, false);
                using (BinaryReader br = new BinaryReader(ms))
                {
                    ms = null;
                    if (command.CommandType == (byte)CommandId.GetUnitInfo)
                    {
                        GetUnitInfo unitInfoCommand = (GetUnitInfo)command;
                        this.unitInfo = unitInfoCommand.ParseParameters(br);
                    }
                    else if (command.CommandType == (byte)CommandId.GetSensorInfo)
                    {
                        GetSensorInfo sensorInfoCommand = (GetSensorInfo)command;
                        this.sensorInfo = sensorInfoCommand.ParseParameters(br);
                    }
                }
            }
            finally
            {
                if (ms != null)
                {
                    ms.Dispose();
                }
            }
        }

        /// <summary>
        /// Called when [write completion].
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="eventArgs">The <see cref="MarsTpl.Api.Transport.AsyncEventArgs"/> instance containing the event data.</param>
        private void OnWriteCompletion(object sender, AsyncEventArgs eventArgs)
        {
        }

        /// <summary>
        /// Handles a HostHeartbeat event
        /// </summary>
        /// <param name="source">The parameter is not used.</param>
        /// <param name="e">The parameter is not used.</param>
        private void OnHostHeartbeatEvent(object source, ElapsedEventArgs e)
        {
            // TODO:  Replace faked temperatures with actual board temperatures from heartbeat
            this.DispatchCommand(new HostHeartbeat(40, 40));
        }

        /// <summary>
        /// The IDisable used for unsubscribing to HeartbeatState
        /// </summary>
        private class Unsubscriber : IDisposable
        {
            /// <summary>
            /// The list of observers
            /// </summary>
            private List<IObserver<HeartbeatState>> observers;

            /// <summary>
            /// The observer this handles
            /// </summary>
            private IObserver<HeartbeatState> observer;

            /// <summary>
            /// The syncRoot
            /// </summary>
            private object syncRoot;

            /// <summary>
            /// Initializes a new instance of the <see cref="Unsubscriber" /> class.
            /// </summary>
            /// <param name="observers">The collections of observers</param>
            /// <param name="observer">This observer</param>
            /// <param name="syncRoot">The object used for synchronization</param>
            public Unsubscriber(
                List<IObserver<HeartbeatState>> observers, IObserver<HeartbeatState> observer, object syncRoot)
            {
                this.observers = observers;
                this.observer = observer;
                this.syncRoot = syncRoot;
            }

            /// <summary>
            /// Removes subscriber
            /// </summary>
            public void Dispose()
            {
                lock (this.syncRoot)
                {
                    if (this.observer != null && this.observers.Contains(this.observer))
                    {
                        this.observers.Remove(this.observer);
                    }
                }
            }
        }
    }
}
